/*
 * Copyright (C) 2008 Romain Guy
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.phonebooksharing.android.util;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;

import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.util.Log;

import com.phonebooksharing.android.widgets.drawable.FastBitmapDrawable;

public class GalleryUtilities {
    
    private static final boolean FLAG_DECODE_BITMAP_WITH_SKIA = false;
    
    private static final float EDGE_START = 0.0f;
    private static final float EDGE_END = 4.0f;
    private static final int EDGE_COLOR_START = 0x7F000000;
    private static final int EDGE_COLOR_END = 0x00000000;
    private static final Paint EDGE_PAINT = new Paint();

    private static final int END_EDGE_COLOR_START = 0x00000000;
    private static final int END_EDGE_COLOR_END = 0x4F000000;
    private static final Paint END_EDGE_PAINT = new Paint();

    private static final float FOLD_START = 5.0f;
    private static final float FOLD_END = 13.0f;
    private static final int FOLD_COLOR_START = 0x00000000;
    private static final int FOLD_COLOR_END = 0x26000000;
    private static final Paint FOLD_PAINT = new Paint();

    private static final float SHADOW_RADIUS = 12.0f;
    private static final int SHADOW_COLOR = 0x99000000;
    private static final Paint SHADOW_PAINT = new Paint();

    private static final Paint SCALE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG |
            Paint.FILTER_BITMAP_FLAG);

    private static volatile Matrix sScaleMatrix;
    
    private static final ConcurrentHashMap<Long, SoftReference<FastBitmapDrawable>> 
                            imageCache = new ConcurrentHashMap<Long, SoftReference<FastBitmapDrawable>>(),
                            videoThumbCache = new ConcurrentHashMap<Long, SoftReference<FastBitmapDrawable>>(),
                            postcardThumbCache  = new ConcurrentHashMap<Long, SoftReference<FastBitmapDrawable>>(),
                            contactImageCache = new ConcurrentHashMap<Long, SoftReference<FastBitmapDrawable>>();
    private static final ConcurrentHashMap<String, SoftReference<FastBitmapDrawable>> 
                            wishlistCache  = new ConcurrentHashMap<String, SoftReference<FastBitmapDrawable>>();
    
    public static FastBitmapDrawable loadingBitmap,
                                     noPictureBitmap;

    static {
        Shader shader = new LinearGradient(EDGE_START, 0.0f, EDGE_END, 0.0f, EDGE_COLOR_START,
                EDGE_COLOR_END, Shader.TileMode.CLAMP);
        EDGE_PAINT.setShader(shader);

        shader = new LinearGradient(EDGE_START, 0.0f, EDGE_END, 0.0f, END_EDGE_COLOR_START,
                END_EDGE_COLOR_END, Shader.TileMode.CLAMP);
        END_EDGE_PAINT.setShader(shader);

        shader = new LinearGradient(FOLD_START, 0.0f, FOLD_END, 0.0f, new int[] {
                FOLD_COLOR_START, FOLD_COLOR_END, FOLD_COLOR_START
        }, new float[] { 0.0f, 0.5f, 1.0f }, Shader.TileMode.CLAMP);
        FOLD_PAINT.setShader(shader);

        SHADOW_PAINT.setShadowLayer(SHADOW_RADIUS / 2.0f, 0.0f, 0.0f, SHADOW_COLOR);
        SHADOW_PAINT.setAntiAlias(true);
        SHADOW_PAINT.setFilterBitmap(true);
        SHADOW_PAINT.setColor(0xFF000000);
        SHADOW_PAINT.setStyle(Paint.Style.FILL);
    }

    private GalleryUtilities() {
    }
    
    /**
     * Loads an image from the specified URL with the specified cookie.
     *
     * @param url The URL of the image to load.
     *
     * @return The image at the specified URL or null if an error occured.
     */
    public static void downloadAndStore(String url, File downloadFile) {
        final HttpGet get = new HttpGet(url);
        get.setHeader("Accept-Encoding", "gzip");

        HttpEntity entity = null;
        try {
            final HttpResponse response = HttpManager.execute(get);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {

                entity = response.getEntity();

                InputStream in = null;
                OutputStream out = null;

                try {
                    in = new GZIPInputStream(entity.getContent());
                    final FileOutputStream fos = new FileOutputStream(downloadFile);
                    out = new BufferedOutputStream(fos, IOUtilities.IO_BUFFER_SIZE);
                    IOUtilities.copy(in, out);
                    out.flush();
                } catch (IOException e) {
                    android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
                } finally {
                    IOUtilities.closeStream(in);
                    IOUtilities.closeStream(out);
                    
                    in  = null;
                    out = null;
                }
            }
        } catch (IOException e) {
            android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
        } finally {
            if (entity != null) {
                try {
                    entity.consumeContent();
                } catch (IOException e) {
                    android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
                }
            }
        }
    }
    
    public static Bitmap load(String url) {
        return load(url, false);
    }
    
    /**
     * Loads an image from the specified URL with the specified cookie.
     *
     * @param url The URL of the image to load.
     * @param isExternal 
     *
     * @return The image at the specified URL or null if an error occured.
     */
    public static Bitmap load(String url, boolean isExternal) {
        Log.d(Global.LOG_TAG, "Loading the following image = " + url);
        Bitmap bitmap = null;

        final HttpGet get = new HttpGet(url);

        HttpEntity entity = null;
        try {
            final HttpResponse response = HttpManager.execute(get);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {

                entity = response.getEntity();

                InputStream in = null;
                OutputStream out = null;

                try {
                    if(!isExternal) {
                        in = new GZIPInputStream(entity.getContent());
                    } else {
                        in = entity.getContent();
                    }
                    if (FLAG_DECODE_BITMAP_WITH_SKIA) {
                        bitmap = BitmapFactory.decodeStream(in);
                    } else {
                        final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
                        //out = new GZIPOutputStream(new BufferedOutputStream(dataStream, IOUtilities.IO_BUFFER_SIZE));
                        out = new BufferedOutputStream(dataStream, IOUtilities.IO_BUFFER_SIZE);
                        IOUtilities.copy(in, out);
                        out.flush();

                        final byte[] data = dataStream.toByteArray();
                        bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                    }
                } catch (IOException e) {
                    android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
                } finally {
                    IOUtilities.closeStream(in);
                    IOUtilities.closeStream(out);
                    
                    in  = null;
                    out = null;
                }
            }
        } catch (IOException e) {
            android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
        } finally {
            if (entity != null) {
                try {
                    entity.consumeContent();
                } catch (IOException e) {
                    android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
                }
            }
        }

        return bitmap;
    }

    /**
     * Return the same image with a shadow, scaled by the specified amount..
     *
     * @param bitmap The bitmap to decor with a shadow
     * @param width The target width of the decored bitmap
     * @param height The target height of the decored bitmap
     *
     * @return A new Bitmap based on the original bitmap
     */
    public static Bitmap createShadow(Bitmap bitmap, int width, int height) {
        if (bitmap == null) return null;

        final int bitmapWidth = bitmap.getWidth();
        final int bitmapHeight = bitmap.getHeight();

        final float scale = Math.min((float) width / (float) bitmapWidth,
                (float) height / (float) bitmapHeight);

        final int scaledWidth = (int) (bitmapWidth * scale);
        final int scaledHeight = (int) (bitmapHeight * scale);

        return createScaledBitmap(bitmap, scaledWidth, scaledHeight,
                SHADOW_RADIUS, false, SHADOW_PAINT);
    }

    
    public static Bitmap scaleBitmap(Bitmap bitmap, int width, int height) {
        if (bitmap == null) return null;

        final int bitmapWidth = bitmap.getWidth();
        final int bitmapHeight = bitmap.getHeight();

        final float scale = Math.min((float) width / (float) bitmapWidth,
                (float) height / (float) bitmapHeight);

        final int scaledWidth = (int) (bitmapWidth * scale);
        final int scaledHeight = (int) (bitmapHeight * scale);
        
        return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
    }
    /**
     * Scales and frame specified bitmap. This method applies several
     * lighting effects to the original bitmap and returns a new decored bitmap.
     *
     * @param bitmap The bitmap to decor with lighting effects
     * @param width The target width of the decored bitmap
     * @param height The target height of the decored bitmap
     *
     * @return A new Bitmap based on the original bitmap
     */
    public static Bitmap scaleAndFrame(Bitmap bitmap, int width, int height) {
        final int bitmapWidth = bitmap.getWidth();
        final int bitmapHeight = bitmap.getHeight();

        final float scale = Math.min((float) width / (float) bitmapWidth,
                (float) height / (float) bitmapHeight);

        final int scaledWidth = (int) (bitmapWidth * scale);
        final int scaledHeight = (int) (bitmapHeight * scale);

        final Bitmap decored = createScaledBitmap(bitmap, scaledWidth, scaledHeight,
                SHADOW_RADIUS, true, SHADOW_PAINT);
        final Canvas canvas = new Canvas(decored);

        canvas.translate(SHADOW_RADIUS / 2.0f, SHADOW_RADIUS / 2.0f);
        canvas.drawRect(EDGE_START, 0.0f, EDGE_END, scaledHeight, EDGE_PAINT);
        canvas.drawRect(FOLD_START, 0.0f, FOLD_END, scaledHeight, FOLD_PAINT);
        //noinspection PointlessArithmeticExpression
        canvas.translate(scaledWidth - (EDGE_END - EDGE_START), 0.0f);
        canvas.drawRect(EDGE_START, 0.0f, EDGE_END, scaledHeight, END_EDGE_PAINT);

        return decored;
    }
    
    private static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight,
            float offset, boolean clipShadow, Paint paint) {
        
        Matrix m;
        synchronized (Bitmap.class) {
            m = sScaleMatrix;
            sScaleMatrix = null;
        }

        if (m == null) {
            m = new Matrix();
        }

        final int width = src.getWidth();
        final int height = src.getHeight();
        final float sx = dstWidth  / (float) width;
        final float sy = dstHeight / (float) height;
        m.setScale(sx, sy);

        Bitmap b = createBitmap(src, 0, 0, width, height, m, offset, clipShadow, paint);

        synchronized (Bitmap.class) {
            sScaleMatrix = m;
        }

        return b;
    }

    private static Bitmap createBitmap(Bitmap source, int x, int y, int width,
            int height, Matrix m, float offset, boolean clipShadow, Paint paint) {

        int scaledWidth = width;
        int scaledHeight = height;

        final Canvas canvas = new Canvas();
        canvas.translate(offset / 2.0f, offset / 2.0f);

        Bitmap bitmap;

        final Rect from = new Rect(x, y, x + width, y + height);
        final RectF to = new RectF(0, 0, width, height);

        if (m == null || m.isIdentity()) {
            bitmap = Bitmap.createBitmap(scaledWidth + (int) offset,
                    scaledHeight + (int) (clipShadow ? (offset / 2.0f) : offset),
                    Bitmap.Config.ARGB_8888);
            paint = null;
        } else {
            RectF mapped = new RectF();
            m.mapRect(mapped, to);

            scaledWidth = Math.round(mapped.width());
            scaledHeight = Math.round(mapped.height());

            bitmap = Bitmap.createBitmap(scaledWidth + (int) offset,
                    scaledHeight + (int) (clipShadow ? (offset / 2.0f) : offset),
                    Bitmap.Config.ARGB_8888);
            canvas.translate(-mapped.left, -mapped.top);
            canvas.concat(m);
        }

        canvas.setBitmap(bitmap);
        canvas.drawRect(0.0f, 0.0f, width, height, paint);
        canvas.drawBitmap(source, from, to, SCALE_PAINT);

        return bitmap;
    }
    
    /**
     * Retrieves a drawable from the cache, identified by the specified id.
     * If the drawable does not exist in the cache, returns null
     * If the drawable cannot be added to the cache, the specified default drawable is
     * returned.
     *
     * @param id The id of the drawable to retrieve
     * @param defaultCover The default drawable returned if no drawable can be found that
     *         matches the id
     *
     * @return The drawable identified by id or defaultCover
     */
    public static FastBitmapDrawable getFromContactCache(Long id) {
        SoftReference<FastBitmapDrawable> reference = contactImageCache.get(id);
        if (reference != null) {
            return reference.get();
        }
        return null;//TODO: cannot throw null, breaks the entire thing
//        return imageCache.get(Long.MAX_VALUE).get();//return the NONPICUTRE image
    }
    
    /**
     * Retrieves a drawable from the cache, identified by the specified id.
     * If the drawable does not exist in the cache, returns null
     * If the drawable cannot be added to the cache, the specified default drawable is
     * returned.
     *
     * @param id The id of the drawable to retrieve
     * @param defaultCover The default drawable returned if no drawable can be found that
     *         matches the id
     *
     * @return The drawable identified by id or defaultCover
     */
    public static FastBitmapDrawable getFromImageCache(Long id) {
        SoftReference<FastBitmapDrawable> reference = imageCache.get(id);
        if (reference != null) {
            return reference.get();
        }
        return null;//TODO: cannot throw null, breaks the entire thing
//        return imageCache.get(Long.MAX_VALUE).get();//return the NONPICUTRE image
    }
    
    public static FastBitmapDrawable getFromWishlistCache(String id) {
        SoftReference<FastBitmapDrawable> reference = wishlistCache.get(id);
        if (reference != null) {
            return reference.get();
        }
        return null;//TODO: cannot throw null, breaks the entire thing
//        return imageCache.get(Long.MAX_VALUE).get();//return the NONPICUTRE image
    }
    
    /**
     * Retrieves a drawable from the cache, identified by the specified id.
     * If the drawable does not exist in the cache, returns null
     * If the drawable cannot be added to the cache, the specified default drawable is
     * returned.
     *
     * @param id The id of the drawable to retrieve
     * @param defaultCover The default drawable returned if no drawable can be found that
     *         matches the id
     *
     * @return The drawable identified by id or defaultCover
     */
    public static FastBitmapDrawable getFromVideoThumbCache(Long id) {
        SoftReference<FastBitmapDrawable> reference = videoThumbCache.get(id);
        if (reference != null) {
            return reference.get();
        }
        return null;//TODO: cannot throw null, breaks the entire thing
//        return imageCache.get(Long.MAX_VALUE).get();//return the NONPICUTRE image
    }
    
    public static FastBitmapDrawable getFromPostcardThumbCache(Long id) {
        SoftReference<FastBitmapDrawable> reference = postcardThumbCache.get(id);
        if (reference != null) {
            return reference.get();
        }
        return null;//TODO: cannot throw null, breaks the entire thing
//        return imageCache.get(Long.MAX_VALUE).get();//return the NONPICUTRE image
    }

    /**
     * Adds a drawable to the cache, identified by the specified id.
     *
     * @param id The id of the drawable to add
     * @param defaultCover The default drawable returned if no drawable can be found that
     *         matches the id
     */
    public static void addToContactCache(Long id, Bitmap bitmap) {
        FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
        contactImageCache.remove(id);
        contactImageCache.put(id, new SoftReference<FastBitmapDrawable>(drawable));
    }
    
    /**
     * Adds a drawable to the cache, identified by the specified id.
     *
     * @param id The id of the drawable to add
     * @param defaultCover The default drawable returned if no drawable can be found that
     *         matches the id
     */
    public static void addToImageCache(Long id, Bitmap bitmap) {
        FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
        imageCache.remove(id);
        imageCache.put(id, new SoftReference<FastBitmapDrawable>(drawable));
    }
    
    public static void addToVideoThumbCache(Long id, Bitmap bitmap) {
        FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
        videoThumbCache.remove(id);
        videoThumbCache.put(id, new SoftReference<FastBitmapDrawable>(drawable));
    }
    
    public static void addToPostcardThumbCache(Long id, Bitmap bitmap) {
        FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
        postcardThumbCache.remove(id);
        postcardThumbCache.put(id, new SoftReference<FastBitmapDrawable>(drawable));
    }
    
    public static void addToWishlistThumbCache(String id, Bitmap bitmap) {
        FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
        wishlistCache.remove(id);
        wishlistCache.put(id, new SoftReference<FastBitmapDrawable>(drawable));
    }
    
    /**
     * Removes all the callbacks from the drawables stored in the memory cache. This
     * method must be called from the onDestroy() method of any activity using the
     * cached drawables. Failure to do so will result in the entire activity being
     * leaked.
     */
    public static void cleanupCache() {
        Log.d(Global.LOG_TAG, "Cleaning up image cache...");
        for (SoftReference<FastBitmapDrawable> reference : contactImageCache.values()) {
            final FastBitmapDrawable drawable = reference.get();
            if (drawable != null) drawable.setCallback(null);
        }
        
        for (SoftReference<FastBitmapDrawable> reference : imageCache.values()) {
            final FastBitmapDrawable drawable = reference.get();
            if (drawable != null) drawable.setCallback(null);
        }
        
        for (SoftReference<FastBitmapDrawable> reference : videoThumbCache.values()) {
            final FastBitmapDrawable drawable = reference.get();
            if (drawable != null) drawable.setCallback(null);
        }
        
        for (SoftReference<FastBitmapDrawable> reference : postcardThumbCache.values()) {
            final FastBitmapDrawable drawable = reference.get();
            if (drawable != null) drawable.setCallback(null);
        }
        
        for (SoftReference<FastBitmapDrawable> reference : wishlistCache.values()) {
            final FastBitmapDrawable drawable = reference.get();
            if (drawable != null) drawable.setCallback(null);
        }
        
        if(loadingBitmap != null) loadingBitmap.setCallback(null);
        if(noPictureBitmap != null) noPictureBitmap.setCallback(null);
        
        Log.d(Global.LOG_TAG, "Images cache clean...");
    }
    
    public static void uploadBinaryStream(String url, File uploadFile) {
        final HttpPost post = new HttpPost(url);

        HttpEntity entity = null;
        try {
            FileEntity fileEntity = new FileEntity(uploadFile, "binary/octet-stream");
            post.setEntity(fileEntity);
            fileEntity.setContentType("binary/octet-stream");

            HttpResponse response = HttpManager.execute(post);
            entity = response.getEntity();

        } catch (IOException e) {
            android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
        } finally {
            if (entity != null) {
                try {
                    entity.consumeContent();
                } catch (IOException e) {
                    android.util.Log.e(Global.LOG_TAG, "Could not load image from " + url, e);
                }
            }
        }
    }

    /**
     * 
     * Create a video thumbnail for a video. May return null if the video is
     * corrupt.
     * 
     * @param filePath
     */

    public static Bitmap createVideoThumbnail(String filePath) {
        Bitmap bitmap = null;
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
            retriever.setDataSource(filePath);
            bitmap = retriever.captureFrame();
        } catch (IllegalArgumentException ex) {
            // Assume this is a corrupt video file
        } catch (RuntimeException ex) {
            // Assume this is a corrupt video file.
        } finally {
            try {
                retriever.release();
            } catch (RuntimeException ex) {
                // Ignore failures while cleaning up.
            }
        }
        return bitmap;
    }
    
    public static Bitmap getBitmapFromFile(ContentResolver ce, File file) {
        return getBitmapFromFile(ce, file, 0, 0);
    }
    
    public static Bitmap getBitmapFromFile(ContentResolver ce, File file, int h, int w) {
        final Uri photoUri = Uri.fromFile(file);
        InputStream photoStream = null;
        try {
            photoStream = ce.openInputStream(photoUri);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 2;
        Bitmap photoBitmap = BitmapFactory.decodeStream(photoStream, null, options);
        int ph, pw;
        if(h > 0 && w > 0) {
            ph = h;
            pw = w;
        } else {
            ph = photoBitmap.getHeight();
            pw = photoBitmap.getWidth();
        }
        if ((pw > ph) && (pw > 128)) {
            double ratio = 128d / pw;
            pw = 128;
            ph = (int) (ratio * ph);
        } else if ((ph > pw) && (ph > 128)) {
            double ratio = 128d / ph;
            ph = 128;
            pw = (int) (ratio * pw);
        }
        final Bitmap scaled = Bitmap.createScaledBitmap(photoBitmap, pw, ph, true);
        photoBitmap.recycle();
        photoBitmap = null;
        
        return scaled;
    }
}
